跳到主要内容

Java Servlet 学习之工作流程

Servlet 生命周期

Servlet 体系结构是建立在 Java 多线程机制上的,它的生命周期由 Web 容器负责。当客户端第一次请求某个 Servlet 时,Servlet 容器将会根据 web.xml 的配置文件实例化这个 Servlet 类。

当有新的客户端请求该 Servlet 时,一般不会再实例化该 Servlet 类。当有多个请求时,Servlet 容器会起多个线程来访问同一个 Servlet 实例的 service() 方法,如果该 Servlet 实例中有共享的实例变量,需要注意多线程安全问题。

所以变量需要使用 ThreadLocal 修饰 ~

Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:

  • Servlet 初始化后调用 init() 方法。
  • Servlet 调用 service() 方法来处理客户端的请求。
  • Servlet 销毁前调用 destroy() 方法。
  • 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

下图显示了一个典型的 Servlet 生命周期方案:

  • 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
  • Servlet 容器在调用 service() 方法之前加载 Servlet。
  • 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。

init() 方法

init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。

Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是也可以指定 Servlet 在服务器第一次启动时被加载。

当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。

init 方法的定义如下:

// 可以重写父类的 init 方法
@Override
public void init() throws ServletException {
// do something
super.init();
System.out.println("初始化完成~");
}

service() 方法

service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。

下面是该方法的特征:

public void service(ServletRequest request, 
ServletResponse response)
throws ServletException, IOException{
}

service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。

说白了就是它会根据请求的类型自动转发给对应的 Get、POST 方法,当然也可以自己重写这个方法,做一些全局操作(比如每个请求前加个日志)

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("当前请求的路径是:" + req.getRequestURI());
super.service(req, resp);
}

所以,不用对 service() 方法做任何动作,只需要根据来自客户端的请求类型来重写 doGet()doPost() 即可。

doGet() 方法

GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。

public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Servlet 代码
}

doPost() 方法

POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。

public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Servlet 代码
}

destroy() 方法

当服务器不再需要 Servlet 实例或重新装入时,会调用 destroy 方法去销毁 servlet,使用这个方法,Servlet 可以释放掉所有在 init 方法申请的资源。

一个 Servlet 实例一旦终止,就不允许再次被调用,只能等待被卸载。

destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示:

public void destroy() {
// 终止化代码...
}

不过因为 Servlet 是单例,所以一般只有容器关闭或者手动调用 destroy() 方法销毁

Servlet 编写过滤器

其实这就是设计模式中的拦截器模式

Servlet 过滤器可以动态地拦截请求和响应,以变换或使用包含在请求或响应中的信息。

可以将一个或多个 Servlet 过滤器附加到一个 Servlet 或一组 Servlet。Servlet 过滤器也可以附加到 JavaServer Pages (JSP) 文件和 HTML 页面。调用 Servlet 前调用所有附加的 Servlet 过滤器。

Servlet 过滤器是可用于 Servlet 编程的 Java 类,可以实现以下目的:

  • 在客户端的请求访问后端资源之前,拦截这些请求。
  • 在服务器的响应发送回客户端之前,处理这些响应。

常用的过滤器有如下(与 Spring 中的拦截器的区别下面会讲):

  • 身份验证过滤器(Authentication Filters)。
  • 数据压缩过滤器(Data compression Filters)。
  • 加密过滤器(Encryption Filters)。
  • 触发资源访问事件过滤器。
  • 图像转换过滤器(Image Conversion Filters)。
  • 日志记录和审核过滤器(Logging and Auditing Filters)。
  • MIME-TYPE 链过滤器(MIME-TYPE Chain Filters,就是文件类型)。
  • 标记化过滤器(Tokenizing Filters)。
  • XSL/T 过滤器(XSL/T Filters),转换 XML 内容。

过滤器通过 Web 部署描述符(web.xml)中的 XML 标签来声明,然后映射到应用程序的部署描述符中的 Servlet 名称或 URL 模式。

当 Web 容器启动 Web 应用程序时,它会为您在部署描述符中声明的每一个过滤器创建一个实例。

Filter 的执行顺序与在 web.xml 配置文件中的配置顺序一致,一般把 Filter 配置在所有的 Servlet 之前。

Servlet 过滤器方法

过滤器是一个实现了 javax.servlet.Filter 接口的 Java 类。这个接口定义了三个方法:

方法一:该方法完成实际的过滤操作,当客户端请求方法与过滤器设置匹配的 URL 时,Servlet容器将先调用过滤器的 doFilter 方法。FilterChain 用户访问后续过滤器。

public void doFilter (ServletRequest, ServletResponse, FilterChain)

方法二:web 应用程序启动时,web 服务器将创建 Filter 的实例对象,并调用其 init 方法,读取 web.xml 配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter 对象只会创建一次,init 方法也只会执行一次)。开发人员通过 init 方法的参数,可获得代表当前 filter 配置信息的 FilterConfig 对象。

public void init(FilterConfig filterConfig)

方法三:Servlet 容器在销毁过滤器实例前调用该方法,在该方法中释放 Servlet 过滤器占用的资源。

public void destroy()

FilterConfig 使用

Filter 的 init 方法中提供了一个 FilterConfig 对象,如 web.xml 文件配置如下:

<filter>
<filter-name>LogFilter</filter-name>
<filter-class>com.example.test.LogFilter</filter-class>
<init-param>
<param-name>Site</param-name>
<param-value>Example</param-value>
</init-param>
</filter>

在 init 方法使用 FilterConfig 对象获取参数:

public void  init(FilterConfig config) throws ServletException {
// 获取初始化参数
String site = config.getInitParameter("Site");
// 输出初始化参数
System.out.println("网站名称: " + site);
}

Servlet 过滤器实例

这里使用注解来注册

//实现 Filter 类,这里使用注解来开发
@WebFilter(initParams = {
// 这是数组
@WebInitParam(name = "Site", value = "Example"),
},
urlPatterns = "/*", filterName = "LogFilter")
public class LogFilter implements Filter {

@Override
public void init(FilterConfig config) throws ServletException {
// 获取初始化参数
String site = config.getInitParameter("Site");
// 输出初始化参数
System.out.println("网站名称: " + site);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

// 真实环境中取得 IP 地址不是这样的,因为这里还有一层 Nginx 转发
System.out.println("当前请求的 IP 是:" + request.getRemoteHost());
// 把请求传回过滤链
chain.doFilter(request, response);
}

}

这个注解的其它属性:

Servlet 过滤器映射

web.xml 中的 Servlet 过滤器映射(Servlet Filter Mapping),就是指定要过滤地址,这里主要讲 XML 的方式配置(实际上可以直接采用注解的方式配置,如上一节所示)

首先定义过滤器,然后映射到一个 URL 或 Servlet,这与定义 Servlet,然后映射到一个 URL 模式方式大致相同。在部署描述符文件 web.xml 中为 filter 标签创建下面的条目:

<?xml version="1.0" encoding="UTF-8"?>  
<web-app>
<filter>
<filter-name>LogFilter</filter-name>
<filter-class>com.example.test.LogFilter</filter-class>
<init-param>
<param-name>Site</param-name>
<param-value>Example</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>LogFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
<!-- 类名 -->
<servlet-name>DisplayHeader</servlet-name>
<!-- 所在的包 -->
<servlet-class>com.runoob.test.DisplayHeader</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>DisplayHeader</servlet-name>
<!-- 访问的网址 -->
<url-pattern>/TomcatTest/DisplayHeader</url-pattern>
</servlet-mapping>
</web-app>

过滤器的应用顺序

在 XML 配置文件中的 filter-mapping 元素的顺序决定了 Web 容器应用过滤器到 Servlet 的顺序。若要反转过滤器的顺序,只需要在 web.xml 文件中反转 filter-mapping 元素即可。

<filter-mapping>
<filter-name>AuthenFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>LogFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

无法在注解的过滤器设置其顺序,不过有个民间说法:

  • 通过 @WebFilter 注解注册的 Filter,其加载顺序与执行顺序无关
  • 通过@WebFilter注解注册的 Filter,其加载顺序与注解的 filterName 值相关(底层通过 HashMap 存储,key 值即 filterName 值)
  • 通过 @WebFilter 注解注册的 Filter,其执行顺序与类名有关,按照类名的字典顺序执行

不过一般多个 Filter 还是在 XML 文件中配置

过滤器和拦截器的区别

注意不要给名字误导了,实际上这两个都是设计模式中的拦截器模式,只是它们的名字有点不同而已 ~

这里再补充一下 Spring 的拦截器和 Servlet 的过滤器的区别

过滤器 和 拦截器 均体现了 AOP 的编程思想,都可以实现诸如日志记录、登录鉴权等功能,但二者的不同点也是比较多的,接下来一一说明。

过滤器的实现原理

过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于 Java的反射机制(动态代理)实现的。

在我们自定义的过滤器中都会实现一个 doFilter() 方法,这个方法有一个 FilterChain 参数:

@Override
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain)
throws IOException, ServletException {
// do something
// 把请求传回过滤链
chain.doFilter(request, response);
}

而实际上它是一个回调接口。ApplicationFilterChain 是它的实现类, 这个实现类内部也有一个 doFilter() 方法就是回调方法。

public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

ApplicationFilterChain 里面能拿到我们自定义的 xxxFilter 类,在其内部回调方法 doFilter() 里调用各个自定义 xxxFilter 过滤器,并执行 doFilter() 方法。

public final class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response) {
...//省略
internalDoFilter(request,response);
}

private void internalDoFilter(ServletRequest request, ServletResponse response){
if (pos < n) {
//获取第pos个filter
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
...
filter.doFilter(request, response, this);
}
}

}

而每个 xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执行 filterChain.doFilter(servletRequest, servletResponse),也就是回调 ApplicationFilterChain的doFilter() 方法,以此循环执行实现函数回调。

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}

拦截器的实现原理

自定义的 Interceptor 类要实现了 Spring 的 HandlerInterceptor 接口。

在继续看之前,先来看下拦截器执行流程图,以此对整个工作方式有个了解(注意,这里是多个拦截器):

继承实现了 HandlerInterceptor 接口的类,比如 Spring 已经提供的实现了 HandlerInterceptor 接口的抽象类 HandlerInterceptorAdapter。

HandlerInterceptor 接口中定义了三个方法,我们就是通过这三个方法来对用户的请求进行拦截处理的。

public class LoginInterceptor implements HandlerInterceptor {

// Controller方法处理请求前执行,根据拦截器定义的顺序,正向执行。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return true;
}

// Controller方法处理请求后执行,根据拦截器定义的顺序,逆向执行。
// 需要所有的 preHandle 方法都返回 true 时才会调用。
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}

// View视图渲染后处理方法:根据拦截器定义的顺序,逆向执行。
// preHandle返回true就会调用。
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}

使用范围不同

我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在 Servlet 规范中定义的,也就是说过滤器 Filter 的使用要依赖于 Tomcat 等容器,导致它只能在 web程序中使用。

而拦截器(Interceptor) 它是一个 Spring 组件,并由 Spring 容器管理,并不依赖 Tomcat 等容器,是可以单独使用的。不仅能应用在 web 程序中,也可以用于 Application、Swing 等程序中。

触发时机不同

过滤器 和 拦截器的触发时机也不同,我们看下边这张图。

过滤器 Filter 是在请求进入容器后,但在进入 servlet 之前进行预处理,请求结束是在 servlet 处理完以后。

拦截器 Interceptor 是在请求进入 servlet 后,在进入 Controller 之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

而 SpringMVC 本质就是一个超级 Servlet,所以 Filter 是处理进入 Spring 容器前的东西

Reference

参考资料 过滤器 和 拦截器 6个区别,别再傻傻分不清了 参考资料 Servlet 的生命周期 参考资料 菜鸟教程的 Java Servlet 教程